8.3. Templates
In one glance
- You will: Decide whether to fork this repository once or turn it into a project generator, and copy its issue and pull-request forms either way.
- You need: Nothing beyond a terminal.
- Time: about 13 minutes, concept.
Which reusable templates does this repository already ship?
Four kinds of template already live under .github/, and a derivative repository can copy them almost verbatim.
A template is a reusable starting point: it captures a proven structure once so the next project spends its effort on the new problem instead of rediscovering the same scaffolding.
This repository carries templates at two very different scales. At the small scale they are concrete and already shipped: the GitHub issue and pull-request forms under .github/ that shape every contribution before a maintainer reads it. At the large scale, "template" is a design decision you should make deliberately: whether to turn the whole reference agent into a parameterized project generator, or to fork it once and move on.
The three issue forms are structured GitHub forms, not free text. Each pins a title: prefix and labels: so triage is automatic, and required validations force the fields a maintainer actually needs.
| Form | Labels and title prefix | Required fields |
|---|---|---|
bug.yml |
bug label, bug: subject |
affected area (a dropdown over reference agent, local infrastructure, Kubernetes or GKE manifests, repository tooling, other); what happened; exact reproduce commands with credentials removed; expected behavior; environment with commit SHA |
feature.yml |
enhancement label, feat: subject |
the learner problem before the preferred tool; the proposed change; the simpler alternatives considered; the upstream OSS license (placeholder Apache-2.0) |
docs.yml |
documentation label, docs: subject |
the page path; a description of what is wrong or unclear; a confirmation that main and the rendered course were checked and that personal data was removed |
The license field is what stops a proposal from smuggling in a paid feature gate. Two fields are invited but optional: the text-rendered log excerpt in bug.yml, and the suggested smallest unblocking change in docs.yml.
Three further templates complete the set:
- Routing.
config.ymlsetsblank_issues_enabled: false, so there is no unstructured escape hatch: every issue enters through a form. Itscontact_linksroute vulnerabilities to a private security mailbox and readers to the rendered course, keeping exploit detail and "is this already fixed?" traffic out of the public tracker. - Pull-request template.
PULL_REQUEST_TEMPLATE.mdasks for What/Why/How and a Test Plan. Its checkboxes are the exact repository gate (mise run format,check,test,scan), plus "ran every changed documentation command from its documented working directory" and "added no credentials, generated reports, or unrelated changes." - Recurring maintenance template.
docs-freshness.mdis a checklist issue for re-verifying time-sensitive claims before each release. It covers model names such asqwen3:4b-instructandgemini-3.6-flash, pinned versions such as agentgatewayv1.4.1and kagent0.9.12, price targets, and measured checkpoints.
The reusable idea is not the wording but the shape: force structure at intake, label for automatic routing, make required fields carry the information a reviewer would otherwise chase, and close the unstructured escape hatch. What a contributor actually does with these forms is 8.5. Contributions.
Should you template or fork the reference agent?
Two honest paths exist for the whole agent, and most learners want the first.
- Fork once. Start from the completed reference on
main, branch, and change one boundary at a time while the gates stay green. This is exactly the path 8.7. Capstone endorses and grades. You keep the git history, the tests, and the infrastructure, and you pay no generator tax: the versions, migrations, and support a template owes every project it stamps out. - Extract a generator. Turn the reference into a parameterized template that stamps out many derivatives: separate repositories generated from one template. Do this only when several agents genuinely need the same packaging, validation, image, protocol, security, and delivery contracts. A generator is another product: it has its own versions, migrations, tests, and support obligations, and every parameter you expose is a promise to keep working.
main is a reference implementation, not a published project generator. If you have one derivative in mind, fork it. Reach for a generator only when maintaining it costs less than hand-porting the same changes into three or more repositories.
Forking? You are done with this page
Copy the .github/ forms above into your own repository and go to 8.7. Capstone. Everything below is for the generator path.
What should be parameterized?
Renaming this agent from "Ops Copilot" to "AgentOps Agent" touched OTEL_SERVICE_NAME, the MLflow experiment, the ADK app_name, the MCP server, and the audit actor. That list is the identity parameter.
In a generator a parameter is any value that legitimately differs between derivatives — identity, provider, ownership, and deployment coordinates — as opposed to structure that should stay fixed. Get this line wrong in either direction and the generator hurts: too few parameters and every derivative hand-edits the same files; too many and the template becomes a fragile configuration language.
For this agent the parameters group into:
- Identity: the distribution/module name, the ADK
app_name, the telemetry service name, the MLflow experiment and prompt-registry names, the audit actor, and the human-facing display name. The CHANGELOG's "Ops Copilot" → "AgentOps Agent" rename is a map of exactly which surfaces an identity change touches. - Model path: the provider, model id, and direct-or-gateway base URL.
- Ownership: which tool, skill, and data packages ship, and who approves writes.
- Serving: the advertised A2A address and the per-request call budget.
- Deployment coordinates: the OCI image name, Kubernetes namespace, local registry, and the optional GCP project/region/zone/bucket.
Never template a credential, a personal path, an existing trace or runtime database, or a hard-coded cloud identity. That rule is already modelled in the reference:
.env.exampleships only placeholders and is copied to a git-ignored.env; install, check, and test tasks never load it.- For the optional Ollama profile,
OPENAI_API_KEY=local-ollamais a non-secret marker the OpenAI SDK requires and Ollama ignores — a real key never appears in the tree. - The GKE overlay mounts no cloud key at all. It uses Workload Identity Federation (WIF), a Kubernetes identity exchanged for a Google one, through
iam.gke.io/gcp-service-accountannotations (0.7. Glossary).
A generator inherits these patterns by copying .env.example (never .env), keeping the marker, and keeping WIF.
Where does each parameter live in this repository today?
Each parameter above lands in specific files. The table below is that map, one row per parameter.
Deeper: where each parameter lives today
A generator's job is to rewrite exactly the cells below and leave everything else alone. Anything not in this table should be an invariant, not a knob.
| Parameter | Where it lives today |
|---|---|
Agent identity (agentops-agent) |
agents/python/pyproject.toml [project].name; ADK app_name and audit actor in server.py/actions.py/mcp_server.py; OTEL_SERVICE_NAME, MLFLOW_EXPERIMENT_NAME in .env.example |
| Course/site identity | root pyproject.toml [project].name = "agentops-open-course"; site config and docs/CNAME |
| Provider, model, base URL | AGENT_MODEL_PROVIDER, AGENT_MODEL, OPENAI_BASE_URL in .env.example; defaults in config.py (ModelProvider, model, openai_base_url) |
| A2A advertised address + call budget | AGENT_A2A_HOST/AGENT_A2A_PORT/AGENT_A2A_PROTOCOL and AGENT_A2A_MAX_LLM_CALLS; the a2a_* fields in config.py |
| OCI image name | the agentops-agent artifact in infra/skaffold.yaml and the build:agent-image task |
| Kubernetes namespace | agentops in infra/k8s/base/namespace.yaml |
| Local registry | registry.localhost:5050 (the platform:dev task) |
| GCP project/region/zone | infra/gcp/terraform.tfvars.example (project_id, region, zone) |
| GCS artifact bucket + WIF identity | GKE overlay placeholders resolved by infra/scripts/render-gke.sh from OpenTofu outputs |
What should remain invariant?
Parameterize identity and coordinates; freeze everything that makes the agent correct and operable. The point of a template is that its structure is trustworthy. Freeze:
- the
src/package layout and the locked dependency workflow (uv sync --locked); - the one
mise runtask vocabulary that hooks, CI, and this course all reuse — see 8.5. Contributions and 8.4. Documentation; - the typed settings/domain/tool boundaries;
- the immutable seed versus writable runtime state split (
agents/data/incidents.dbis never mutated); - direct local reads versus governed MCP reads and confirmed, audited writes;
- offline branch-covered tests and deterministic adversarial cases;
- the non-root image with explicit health, resource, storage, and NetworkPolicy declarations;
- the docs/source synchronization contract and the OSS-boundary language that keeps optional cloud clearly labelled.
These are the same invariants the capstone must preserve; a generator simply refuses to make them parameters.
Which OSS templating tool should you use?
Pick Copier when generated projects must keep receiving template updates, Cookiecutter when a create-once scaffold is enough.
Deeper: the two tools, and what to check before adopting one
Copier is an open-source option when generated projects must keep receiving template updates. Cookiecutter is a simpler open-source generator for create-once scaffolds. Review licenses and dependencies and pin the selected tool; neither is required by this repository.
How do you validate a generated project?
You test the rendered project, not the template.
Generate into a disposable directory, run the reference's own gate against the output, then grep it for leftovers: unfilled template delimiters (the placeholder markers a generator replaces) and the source identity.
Deeper: the full validation loop
A generator is only correct if its output is correct, so you test the rendered project, not the template. The failure mode is a template that renders but produces a project that only builds inside the original author's workspace — leftover delimiters, dangling references to the source identity, or fixture data that was never meant to ship.
Generate into a disposable directory and run the reference's own gate against the output:
mise run installresolves the locked dependency environment.mise run formatthenmise run check— the check step already renders both Kubernetes overlays (check:infra) and runs the offline Markdown link check (check:links), so overlay render and link validation come for free.mise run testruns the offline, branch-covered suite with no model, provider, cluster, or cloud.mise run scanruns gitleaks history plus Trivy vulnerabilities, secrets, licenses, and misconfiguration.
Then grep the rendered tree for anything that should have been replaced: unreplaced template delimiters, the source identities agentops-agent and agentops-open-course, the maintainer email and GCP project, and the reference domain inside agents/data/ — the INC-* incident ids, the eight service names, and the runbook slugs.
What you must not do is empty agents/data/. The offline suite copies that directory for every test through an autouse fixture, so a project shipped without a dataset cannot pass mise run test at all. A generator therefore renders a replacement seed — its own sql/schema.sql, sql/seed.sql, rebuilt incidents.db, and the runbooks and logs those rows reference — and proves it with mise run check:data (8.7. Capstone owns that contract). The generated repository must pass its own gate and be usable without any access to the original workspace.
flowchart TD
A[Parameterize] --> B[Render into a disposable directory]
B --> C["mise run install / format / check / test / scan"]
C --> D{Both overlays render and offline links pass?}
D -- no --> A
D -- yes --> E[Grep for leftover delimiters and source identity]
E --> F{Any agentops-agent / agentops-open-course / INC-* leaks?}
F -- yes --> A
F -- no --> G[Usable without the original workspace: pass]
What proves this page worked?
Forking? You have nothing to prove here. Copy .github/ISSUE_TEMPLATE/ and .github/PULL_REQUEST_TEMPLATE.md into your own repository and move on.
Building a generator? Write the parameter table above and the expected generated tree, then prove that two different parameter sets both pass the complete gate from a clean checkout. Do not publish the generator until its update/migration behavior and license notices are themselves tested — a published generator promises those to every derivative.
This page owns the structure and enforcement of both scales. Three siblings carry the rest:
- 8.5. Contributions — the workflow that fills the forms in.
- 8.7. Capstone — the recommended way to build your own agent from this one.
- 8.4. Documentation — the docs contract those templates protect.
You are done when:
- Your own repository has the issue forms, a
config.ymlsettingblank_issues_enabled: false, and a pull-request template whose checkboxes are your gate. - You can say which path you are on, fork or generator, and name the cost you accepted.
- Generator path only: two different parameter sets each render a project that passes
mise run install,format,check,test, andscanfrom a clean checkout. - Generator path only: a grep of each rendered tree returns no
agentops-agent, noagentops-open-course, and none of the reference domain (INC-*, its service names, its runbook slugs) — whileagents/data/carries the derivative's own seed andmise run check:datapasses against it.
Return to 8. Community and pick your next maintenance question when you have decided, in one sentence, whether you are forking this repository once or maintaining a generator.